package org.hawk.gwt.ppc.loader;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Composite sources loader that delegates all operations to 
 * correct loaders for each path element.
 * @author alex.bereznevatiy@gmail.com
 */
public class UniversalSourcesLoader implements SourcesLoader {
	
	private List<SourcesLoader> loaders;
	
	/**
	 * Creates loader for the classpath elements passed.
	 * @param classPath
	 * @throws IOException in case of unable to open jar
	 * @throws IllegalArgumentException in case of 
	 * not supported path element
	 */
	public UniversalSourcesLoader(String[] classPath) throws IOException{
		this(toURIs(classPath));
	}
	
	/**
	 * Creates loader for the default classpath of current 
	 * VM run. All classes and sources will be loaded from
	 * current classloaders set.
	 * @throws IOException
	 */
	public UniversalSourcesLoader() throws IOException {
		this(resolveClassPath());
	}
	
	/**
	 * Creates loader for the classpath elements passed.
	 * @param classPath
	 * @throws IOException in case of unable to open jar
	 * @throws IllegalArgumentException in case of 
	 * not supported path element
	 */
	public UniversalSourcesLoader(URI[] classPath) throws IOException {
		loaders = new LinkedList<SourcesLoader>();
		resolve(classPath);
	}
	
	private static URI[] toURIs(String[] classPath) {
		URI[] result = new URI[classPath.length];
		
		for (int i = 0; i < classPath.length; i++) {
			result[i] = new File(classPath[i]).toURI();
		}
		return result;
	}

	private static URI[] resolveClassPath() throws MalformedURLException {
		Set<URI> result = new HashSet<URI>();
		
		ClassLoader loader = UniversalSourcesLoader.class.getClassLoader();
		if (loader instanceof URLClassLoader) {
			resolveClassLoaderClassPath(result, (URLClassLoader) loader);
		}
		
		resolveEnvClassPath(result);
		
		return result.toArray(new URI[result.size()]);
	}

	private static void resolveEnvClassPath(Set<URI> result) {
		Properties env = System.getProperties();
		String prop = env.getProperty("java.class.path", "").trim();
		if (prop.length() == 0) {
			return;
		}
		
		for (String url : prop.split(File.pathSeparator)) {
			result.add(new File(url).toURI());
		}
	}

	private static void resolveClassLoaderClassPath(Set<URI> result, URLClassLoader classLoader) throws MalformedURLException {
		for (URL url : classLoader.getURLs()) {
			try {
				result.add(url.toURI());
			} catch(URISyntaxException ex) {
				throw new MalformedURLException(ex.toString());
			}
		}
	}

	private void resolve(URI[] classPath) throws IOException {
		StringBuilder unsupported = new StringBuilder();
		for (URI element : classPath) {
			resolve(unsupported, element);
		}
		if (unsupported.length() > 0) {
			unsupported.append(']');
			throw new IllegalArgumentException("Unsupported path element(s): [" + unsupported);
		}
	}

	private void resolve(StringBuilder unsupported, URI element) throws IOException {
		File file = new File(element);
		if (file.isDirectory()) {
			loaders.add(new FileSystemSourcesLoader(file));
			return;
		} 
		if (file.isFile() && element.getPath().endsWith(".jar")) {
			loaders.add(new JarSourcesLoader(file));
			return;
		}
		if (unsupported.length() > 0) {
			unsupported.append("], [");
		}
		unsupported.append(element);
	}
	
	/**
	 * @return loaders used by this universal loader to
	 * resolve sources within classpath.
	 */
	public List<SourcesLoader> getClassPath() {
		return Collections.unmodifiableList(loaders);
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getCompilationUnits(java.lang.String)
	 */
	public List<String> getCompilationUnits(String packageName) {
		ArrayList<String> result = new ArrayList<String>();
		for (SourcesLoader loader : loaders) {
			List<String> classes = new ArrayList<String>(loader.getCompilationUnits(packageName));
			classes.removeAll(result);
			result.addAll(classes);
		}
		result.trimToSize();
		return Collections.unmodifiableList(result);
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getCompilationUnit(java.lang.String)
	 */
	public URL getCompilationUnit(String name) {
		for (SourcesLoader loader : loaders) {
			URL rez = loader.getCompilationUnit(name);
			if(rez != null) {
				return rez;
			}
		}
		
		return null;
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getSubpackages(java.lang.String)
	 */
	public List<String> getSubpackages(String packageName) {
		ArrayList<String> result = new ArrayList<String>();
		for(SourcesLoader loader : loaders) {
			List<String> packages = new ArrayList<String>(loader.getSubpackages(packageName));
			packages.removeAll(result);
			result.addAll(packages);
		}
		result.trimToSize();
		return Collections.unmodifiableList(result);
	}
	
	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.SourcesLoader#isExists(java.lang.String)
	 */
	public boolean isExists(String resourceName) {
	 	for (SourcesLoader loader : loaders) {
			if (loader.isExists(resourceName)) {
				return true;
			}
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getResource(java.lang.String)
	 */
	public URL getResource(String name) {
		for (SourcesLoader loader : loaders) {
			URL rez = loader.getResource(name);
			if (rez != null) {
				return rez;
			}
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "UniversalSourcesLoader[" + loaders + "]";
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#toUrl()
	 */
	public URL toUrl() throws PreprocessorException {
		throw new PreprocessorException("Unsupported operation");
	}
	
	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof UniversalSourcesLoader) {
			UniversalSourcesLoader other = (UniversalSourcesLoader) obj;
			return other.loaders.size() == loaders.size() &&
				other.loaders.containsAll(loaders);
		} else if (obj instanceof SourcesLoader) {
			return loaders.size() == 1 && loaders.get(0).equals(obj);
		}
		return false;
	}
}
